#include <connection.h>
#include <debug.h>
#include <eventloop.h>
#include <imgstore.h>
#include <util.h>

#include <inttypes.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>

#include "delta-connection.h"
#include "libdelta.h"

#define IMEX_RECEIVED_MESSAGE "Setup message received. To apply it, reply with:\nIMEX: %d nnnn-nnnn-nnnn-nnnn-nnnn-nnnn-nnnn-nnnn-nnnn\nNo whitespace in the setup-code!"

void delta_recv_im(DeltaConnectionData *conn, dc_msg_t *msg);

void
_transpose_config(dc_context_t *mailbox, PurpleAccount *acct)
{
	const char *addr = acct->username;
	const char *display = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_DISPLAY_NAME, NULL);

	const char *imap_host = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_IMAP_SERVER_HOST, NULL);
	const char *imap_user = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_IMAP_USER, NULL);
	const char *imap_pass = purple_account_get_password(acct);
	const char *imap_port = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_IMAP_SERVER_PORT, NULL);

	const char *smtp_host = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_SMTP_SERVER_HOST, NULL);
	const char *smtp_user = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_SMTP_USER, NULL);
	const char *smtp_pass = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_SMTP_PASS, NULL);
	const char *smtp_port = purple_account_get_string(acct, PLUGIN_ACCOUNT_OPT_SMTP_SERVER_PORT, NULL);

	gboolean bcc_self = purple_account_get_bool(acct, PLUGIN_ACCOUNT_OPT_BCC_SELF, FALSE);

	dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_ADDR, addr);
	dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_DISPLAY_NAME, display);

	dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_IMAP_SERVER_HOST, imap_host);
	dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_IMAP_USER, imap_user);
	dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_IMAP_PASS, imap_pass);
	dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_IMAP_SERVER_PORT, imap_port);

	dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_SMTP_SERVER_HOST, smtp_host);
	dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_SMTP_USER, smtp_user);
	dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_SMTP_PASS, smtp_pass);
	dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_SMTP_SERVER_PORT, smtp_port);

	if (bcc_self) {
		dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_BCC_SELF, "1");
	} else {
		dc_set_config(mailbox, PLUGIN_ACCOUNT_OPT_BCC_SELF, "0");
	};
}

typedef struct {
	DeltaConnectionData *conn;

	// Used by delta_process_incoming_message
	uint32_t msg_id;
	gboolean msg_changed;

	// Used by delta_process_connection_state
	int connection_state;
} ProcessRequest;

gboolean
delta_process_incoming_message(void *data)
{
	ProcessRequest *pr = (ProcessRequest *)data;
	g_assert(pr != NULL);
	g_assert(pr->conn != NULL);

	dc_msg_t *msg = dc_get_msg(pr->conn->mailbox, pr->msg_id);
	delta_recv_im(pr->conn, msg);
	dc_msg_unref(msg);
	g_free(data);

	return FALSE;
}

gboolean
delta_process_connection_state(void *data)
{
	ProcessRequest *pr = (ProcessRequest *)data;
	g_assert(pr != NULL);
	g_assert(pr->conn != NULL);

	purple_connection_update_progress(
		pr->conn->pc,
		"Connecting...",
		pr->connection_state,
		MAX_DELTA_CONFIGURE
	);

	if (pr->connection_state == MAX_DELTA_CONFIGURE) {
		purple_connection_set_state(pr->conn->pc, PURPLE_CONNECTED);
	}

	g_free(data);

	return FALSE;
}

gboolean
delta_process_fresh_messages(void *data)
{
	ProcessRequest *pr = (ProcessRequest *)data;
	g_assert(pr != NULL);
	g_assert(pr->conn != NULL);

	dc_context_t *mailbox = pr->conn->mailbox;
	g_assert(mailbox != NULL);

	// Spot any messages received while offline
	dc_array_t *fresh_msgs = dc_get_fresh_msgs(mailbox);
	size_t fresh_count = dc_array_get_cnt(fresh_msgs);

	purple_debug_info(PLUGIN_ID, "fresh_count: %zu\n", fresh_count);

	for(size_t i = 0; i < fresh_count; i++) {
		uint32_t msg_id = dc_array_get_id(fresh_msgs, i);
		dc_msg_t *msg = dc_get_msg(mailbox, msg_id);

		delta_recv_im(pr->conn, msg);
	}

	dc_array_unref(fresh_msgs);

	return FALSE;
}

ProcessRequest *
delta_build_process_request(DeltaConnectionData *conn)
{
	g_assert(conn != NULL);

	ProcessRequest *pr = g_malloc(sizeof(ProcessRequest));
	g_assert(pr != NULL);

	pr->conn = conn;

	return pr;
}

// Do not call any libpurple or delta functions in here, as it is not
// thread-safe and events may be dispatched from any delta thread. Use
// purple_timeout_add(0, callback, data) to run on the main thread instead
void *
delta_event_handler(void *context)
{
	DeltaConnectionData *conn = (DeltaConnectionData *)context;

	g_assert(conn != NULL);

	dc_context_t *mailbox = conn->mailbox;
	dc_event_emitter_t* emitter = dc_get_event_emitter(mailbox);
	dc_event_t* event;

	// FIXME: do we still need runthreads?
	while (conn->runthreads && (event = dc_get_next_event(emitter)) != NULL) {
		ProcessRequest *pr = NULL;
		int event_id = dc_event_get_id(event);

		purple_debug_info(PLUGIN_ID, "Event %d received from Delta.\n", event_id);

		switch (event_id) {
		case DC_EVENT_SMTP_MESSAGE_SENT:
		case DC_EVENT_IMAP_CONNECTED:
		case DC_EVENT_SMTP_CONNECTED:
		case DC_EVENT_IMAP_MESSAGE_DELETED:
		case DC_EVENT_IMAP_MESSAGE_MOVED:
		case DC_EVENT_INFO: {
			char *info = dc_event_get_data2_str(event);
			purple_debug_info(PLUGIN_ID, "INFO from Delta: %s\n", info);
			dc_str_unref(info);
			break;
		}
		case DC_EVENT_WARNING: {
			char *warn = dc_event_get_data2_str(event);
			purple_debug_info(PLUGIN_ID, "WARNING from Delta: %s\n", warn);
			dc_str_unref(warn);
			break;
		}
		case DC_EVENT_ERROR:
		case DC_EVENT_ERROR_NETWORK: {
			int errcode = dc_event_get_data1_int(event);
			char *err = dc_event_get_data2_str(event);
			purple_debug_info(PLUGIN_ID, "ERROR from Delta: %d: %s\n", errcode, err);
			dc_str_unref(err);
			break;
		}

		case DC_EVENT_MSGS_CHANGED: {
			// This event may be issued for a single message, in which case the
			// message ID is in data2 and we should treat it as an incoming msg
			// FIXME: this leads to duplicate messages when it's an outgoing
			// message we just sent
			uint32_t msg_id = dc_event_get_data2_int(event);

			pr = delta_build_process_request(conn);
			if (msg_id) {
				// FIXME: for now, only display IMEX setup messages to avoid duplicates
				pr->msg_id = msg_id;
				pr->msg_changed = TRUE;

				purple_timeout_add(0, delta_process_incoming_message, pr);
			} else {
				purple_timeout_add(0, delta_process_fresh_messages, pr);
			}
			break;
		}

		case DC_EVENT_INCOMING_MSG:
			// data1 is chat_id, which we don't seem to need yet.
			// TODO: It may be needed for group chats
			pr = delta_build_process_request(conn);
			pr->msg_id = (uint32_t)dc_event_get_data2_int(event);
			purple_timeout_add(0, delta_process_incoming_message, pr);
			break;

		// Things left to do
		case DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED:
		case DC_EVENT_NEW_BLOB_FILE:
		case DC_EVENT_DELETED_BLOB_FILE:
		case DC_EVENT_MSG_DELIVERED:
		case DC_EVENT_MSG_READ:
		case DC_EVENT_MSG_FAILED:
		case DC_EVENT_CHAT_MODIFIED:
		case DC_EVENT_CONTACTS_CHANGED:
		case DC_EVENT_ERROR_SELF_NOT_IN_GROUP:
		case DC_EVENT_IMEX_FILE_WRITTEN:
		case DC_EVENT_IMEX_PROGRESS:
		case DC_EVENT_LOCATION_CHANGED:
		case DC_EVENT_MSGS_NOTICED:
		case DC_EVENT_SECUREJOIN_INVITER_PROGRESS:
		case DC_EVENT_SECUREJOIN_JOINER_PROGRESS:
			purple_debug_info(PLUGIN_ID, "Event %d is TODO\n", event_id);
			break;

		case DC_EVENT_CONFIGURE_PROGRESS:
			pr = delta_build_process_request(conn);
			pr->connection_state = dc_event_get_data1_int(event);
			purple_timeout_add(0, delta_process_connection_state, pr);
			break;

		default:
			purple_debug_info(PLUGIN_ID, "Unknown Delta event: %d\n", event_id);
		}

		dc_event_unref(event);
	}
	dc_event_emitter_unref(emitter);

	return NULL;
}

void
delta_connection_new(PurpleConnection *pc)
{
	DeltaConnectionData *conn = NULL;

	g_assert(purple_connection_get_protocol_data(pc) == NULL);

	conn = g_new0(DeltaConnectionData, 1);
	conn->pc = pc;
	purple_connection_set_protocol_data(pc, conn);
}

void
delta_connection_free(PurpleConnection *pc)
{
	DeltaConnectionData *conn = purple_connection_get_protocol_data(pc);

	g_assert(conn != NULL);

	conn->runthreads = 0;
	
	if (conn->mailbox != NULL) {
		dc_stop_ongoing_process(conn->mailbox);
		dc_stop_io(conn->mailbox);

		// TODO: correctly handle join failing
		purple_debug_info(PLUGIN_ID, "Joining event thread\n");
		if (pthread_join(conn->event_thread, NULL) != 0) {
			purple_debug_info(PLUGIN_ID, "joining event thread failed\n");
		}

		dc_context_unref(conn->mailbox);
	}

	purple_connection_set_protocol_data(pc, NULL);

	// TODO: free resources as they are added to DeltaConnectionData
	conn->pc = NULL;
	conn->mailbox = NULL;

	g_free(conn);
}

void
delta_connection_start_login(PurpleConnection *pc)
{
	char dbname[1024];
	PurpleAccount *acct = pc->account;
	DeltaConnectionData *conn = purple_connection_get_protocol_data(pc);
	dc_context_t *mailbox = NULL; 

	g_snprintf(
		dbname, 1024, "%s%sdelta_db-%s",
		purple_user_dir(), G_DIR_SEPARATOR_S, acct->username
	);

	mailbox = dc_context_new(PLUGIN_ID, dbname, NULL);

	conn->mailbox = mailbox;
	_transpose_config(mailbox, acct);

	conn->runthreads = 1;
	pthread_create(&conn->event_thread, NULL, delta_event_handler, conn);

	dc_configure(mailbox);
	dc_start_io(mailbox);
	dc_maybe_network(mailbox);

	return;
}

gboolean delta_try_process_imex(dc_context_t *mailbox, char *text) {
	if (!g_str_has_prefix(text, "IMEX: ")) {
		return FALSE;
	}

	gchar **parts = g_strsplit(text, " ", 3);
	if (g_strv_length(parts) != 3) {
		g_strfreev(parts);
		return FALSE;
	}

	int msg_id = atoi(parts[1]);
	gboolean success = dc_continue_key_transfer(mailbox, msg_id, parts[2]);

	g_strfreev(parts);

	return success;
}

int
delta_send_im(PurpleConnection *pc, const char *who, const char *message, PurpleMessageFlags flags)
{
	UNUSED(flags);

	DeltaConnectionData *conn = (DeltaConnectionData *)purple_connection_get_protocol_data(pc);
	g_assert(conn != NULL);

	dc_context_t *mailbox = conn->mailbox;
	g_assert(mailbox != NULL);

	uint32_t contact_id = dc_create_contact(mailbox, NULL, who);
	uint32_t chat_id = dc_create_chat_by_contact_id(mailbox, contact_id);

	GData *attrs;
	const char *msg_ptr, *start, *end;
	msg_ptr = message;

	// Send each image included in the message.
	while (purple_markup_find_tag("img", msg_ptr, &start, &end, &attrs) == TRUE) {
		char *id_str = g_datalist_id_get_data(&attrs, g_quark_from_string("id"));
		purple_debug_info(PLUGIN_ID, "In a loop, got %s\n", id_str);

		msg_ptr = end + 1;

		if (id_str == NULL || strlen(id_str) == 0) {
			g_datalist_clear(&attrs);
			continue;
		}

		int id = atoi(id_str);
		g_datalist_clear(&attrs);

		if (id <= 0) {
			continue;
		}

		GError *err = NULL;
		char *tempdir = g_dir_make_tmp(NULL, &err);
		if (err != NULL) {
			purple_debug_info(PLUGIN_ID, "Couldn't get a temporary dir for image %d: %s", id, err->message);
			g_free(err);
			continue;
		}

		PurpleStoredImage *img = purple_imgstore_find_by_id(id);
		const char *filename = purple_imgstore_get_filename(img);
		const char *extension = purple_imgstore_get_extension(img);
		gconstpointer data = purple_imgstore_get_data(img);

		char *path = g_strdup_printf("%s/%s", tempdir, filename);

		g_file_set_contents(path, data, purple_imgstore_get_size(img), &err);
		if (err != NULL) {
			purple_debug_info(PLUGIN_ID, "failed to write %s to temporary file: %s\n", filename, err->message);
			g_free(err);
			goto next;
		}

		purple_debug_info(PLUGIN_ID, "Sending image %s from imgstore: %d\n", filename, id);

		dc_msg_t *img_msg = dc_msg_new(mailbox, DC_MSG_IMAGE);
		dc_msg_set_file(img_msg, path, extension);
		dc_send_msg(mailbox, chat_id, img_msg);
		dc_msg_unref(img_msg);

next:
		remove(path);
		remove(tempdir);
		g_free(path);
	}

	// Send any text left
	char *stripped_message = purple_markup_strip_html(message);
	g_assert(stripped_message != NULL);
	if (strlen(stripped_message) > 0) {
		if (!delta_try_process_imex(mailbox, stripped_message)) {
			dc_send_text_msg(mailbox, chat_id, stripped_message);
		}
	}
	g_free(stripped_message);

	return 0; // success; don't echo the message to the chat window since we display it anyway
}

void
delta_recv_im(DeltaConnectionData *conn, dc_msg_t *msg)
{
	dc_context_t *mailbox = conn->mailbox;
	g_assert(mailbox != NULL);

	PurpleConnection *pc = conn->pc;
	g_assert(pc != NULL);

	uint32_t msg_id = dc_msg_get_id(msg);
	int viewtype = dc_msg_get_viewtype(msg);
	time_t timestamp = dc_msg_get_timestamp(msg);
	char *text = dc_msg_get_text(msg);
	uint32_t chat_id = dc_msg_get_chat_id(msg);
	dc_contact_t *from = dc_get_contact(mailbox, dc_msg_get_from_id(msg));
	dc_chat_t *chat = dc_get_chat(mailbox, chat_id);
	dc_array_t *contacts = dc_get_chat_contacts(mailbox, chat_id);
	int num_contacts = dc_array_get_cnt(contacts);

	if (chat == NULL) {
		purple_debug_info(PLUGIN_ID, "Receiving IM: unknown chat: %d\n", chat_id);
		goto out;
	}

	if (dc_chat_get_type(chat) == DC_CHAT_TYPE_GROUP) {
		purple_debug_info(PLUGIN_ID, "Receiving IM: group chat with ID %d! Not yet supported\n", chat_id);
		goto out;
	}

	if (num_contacts != 1) {
		purple_debug_info(PLUGIN_ID, "Receiving IM: 1-1 chat %d with %d contacts instead of 1!\n", chat_id, num_contacts);
		goto out;
	}

	// FIXME: using dc_array_get_contact_id fails here, complaining that it's not an array of locations
	dc_contact_t *contact = dc_get_contact(mailbox, dc_array_get_id(contacts, 0));
	char *who = NULL;

	// In the current architecture, delta_send_im and delta_recv_im must agree
	// on the value for 'who'. Using the email address is an easy cheat for this
	// but gets shaky in the long term.
	if (contact != NULL) {
		who = dc_contact_get_addr(contact);
	} else {
		who = dc_chat_get_name(chat);
	}

	int flags = 0;
	int state = dc_msg_get_state(msg);

	if (state == DC_STATE_IN_FRESH || state == DC_STATE_IN_NOTICED || state == DC_STATE_IN_SEEN) {
		flags |= PURPLE_MESSAGE_RECV;
	} else {
		flags |= PURPLE_MESSAGE_SEND;
	}

	// FIXME: as a massive hack, convert IMEX setup messages into a text message
	// prompting the user how to trigger the IMEX filter in outgoing messages.
	if (dc_msg_is_setupmessage(msg)) {
		purple_debug_info(PLUGIN_ID, "Receiving IMEX: ID=%d\n", msg_id);
		viewtype = DC_MSG_TEXT;
		dc_str_unref(text);
		text = g_strndup("", 1024);
		g_assert(text != NULL);

		g_snprintf(text, 1024, IMEX_RECEIVED_MESSAGE, msg_id);
	}

	switch(viewtype) {
	case DC_MSG_GIF:
	case DC_MSG_IMAGE:
	case DC_MSG_STICKER:
		flags = flags | PURPLE_MESSAGE_IMAGES;
		break;
	case DC_MSG_TEXT:
		flags = flags | PURPLE_MESSAGE_RAW;
		break;
	case DC_MSG_VIDEO: // Pidgin only supports these as files for download
	case DC_MSG_FILE:
	case DC_MSG_AUDIO: // Sound to play
	case DC_MSG_VOICE:
		break;
	default:
		purple_debug_info(PLUGIN_ID, "Message %d: unknown message type: %d\n", msg_id, viewtype);
	}

	int image_id = 0;

	if ((flags & PURPLE_MESSAGE_IMAGES) > 0) {
		char *filename = dc_msg_get_file(msg);
		gchar *data;
		gsize length;
		GError *err = NULL;

		g_file_get_contents(filename, &data, &length, &err);
		if (err != NULL) {
			purple_debug_info(PLUGIN_ID, "Failed to read image %s: %s\n", filename, err->message);
			g_error_free(err);
			goto out;
		}

		image_id = purple_imgstore_add_with_id(data, length, filename);
		text = g_strdup_printf("<img id='%d'><br/>%s", image_id, text);
	}

	char *name = dc_contact_get_name(from);
	int msglen = strlen(name) + 3 + strlen(text);

	char *msgtext = malloc(msglen);
	g_snprintf(msgtext, msglen, "%s: %s", name, text);

	serv_got_im(pc, who, msgtext, flags, timestamp);

	if (image_id > 0) {
		purple_imgstore_unref_by_id(image_id);
	}

	dc_markseen_msgs(mailbox, &msg_id, 1);
	g_free(msgtext);
	dc_str_unref(who);
	dc_str_unref(name);
	dc_contact_unref(contact);
out:
	dc_str_unref(text);
	dc_chat_unref(chat);
	dc_array_unref(contacts);
}
